package com.ease.framework {

   import com.ease.util.Diagnostics;
   import com.ease.util.MixedUtil;
   import com.ease.util.ObjectEvent;
   import com.ease.util.ObjectProjection;
   import com.ease.util.ReflectionUtil;
   import com.ease.util.ServiceApplicabilityUpdateEvent;
   import com.ease.util.Set;
   import com.ease.util.TaggedPropertyInfo;
   import com.ease.util.reflection.TagArgReflectionInfo;
   import com.ease.util.gc.SharedReference;

   import flash.events.Event;
   import flash.events.EventDispatcher;
   import flash.events.IEventDispatcher;
   import flash.utils.Dictionary;

   import mx.collections.IList;
   import mx.core.FlexGlobals;
   import mx.core.UIComponent;
   import mx.events.FlexEvent;
   import mx.utils.StringUtil;


   /**
    * Provides a generic and extensible dependency injection service by processing the
    * [Injectable(...)] tag.
    *
    * <p>Object providers or directories that wish to provide injectable objects (i.e.,
    * objects that are injected into other objects) should implement the IObjectDirectory
    * interface and register themselves with this class as a provider.
    *
    * <p>Object registries that contain injection target objects (i.e., objects that get
    * injected with other objects) should implement the IInjectionTargetRegistry interface
    * and register themselves with this class as a consumer.
    *
    * <p>A property is declared to be injectable through the [Injectable] tag. Optional
    * parameters can be used to specify what kind of object/s is/are to be injected and how.
    * In the following example, the requested objects are of the given type such that they
    * have the specified values for the given property and meta-property:
    *
    * <listing version="3.0">
    * [Injectable( type="com.some.ns.SomeType",
    *              selectProperty="someProperty", value="desiredValueForTheProp",
    *              selectMetaProperty="someMetaProperty", value="desiredValueForTheMetaProp" )]
    * [Optionally, some other tags, e.g. Bindable]
    * public var anInjectableProperty:SomePropertyType;
    * </listing>
    *
    * <p>The property type of the injection target would typically be ArrayCollection or some
    * non-collection type whose single instance is sought after.
    *
    * <p>The above example can equivalently be written in the following more compact form:
    *
    * <listing version="3.0">
    * [Injectable( type = "com.some.ns.SomeType",
    *              filter = "someProperty='desiredValueForTheProp' and &#64;someMetaProperty='desiredValueForTheMetaProp'" )]
    * public var anInjectableProperty:SomePropertyType;
    * </listing>
    * where '&#64;' signifies a meta-property.
    *
    * <p>In the above example, if a property on the specified objects (i.e., instances of
    * SomeType) was to be injected rather than the objects themselves, the injectProperty
    * parameter could be used as follows:
    *
    * <listing version="3.0">
    * ...
    * [Injectable( type="com.some.ns.SomeType", injectProperty="someProperty_Of_SomeType",
    *              selectProperty="someProperty", value="desiredValueForTheProp", ... )]
    * public var anInjectableProperty:SomePropertyType;
    * </listing>
    *
    * <p>The <code>value</code>s can also be evaluation tokens, where the token may
    * represent a property of the declaring class. For example:
    *
    * <listing version="3.0">
    * ...
    * [Injectable( ..., selectProperty="someProperty", value="{foo}", ... )]
    * public var anInjectableProperty:SomePropertyType;
    * ...
    * public var foo:SomePropertyType2;
    * </listing>
    *
    * <p>The property foo in the above example could also be an array of the desired values.
    * This would give what can be called a <i>multivalue selection</i>. These dependent
    * injectable properties (anInjectableProperty in this case, being dependent on foo) are
    * re-injected when the depended-upon property (foo in this case) changes. It can also be
    * an indefinitely nested property (e.g., 'foo.bar'), although in that case, changes to
    * only the first component trigger re-injection (e.g., re-injection happens when foo is
    * set with a different value, but not when bar is set by itself).
    *
    * <p>The tokenized value could contain multiple tokens as well:
    *
    * <listing version="3.0">
    * ...
    * [Injectable( ..., selectProperty="someProperty", value="{foo}x{bar}", ... )]
    * public var anInjectableProperty:SomePropertyType;
    * ...
    * public var foo:String;
    * public function get bar():String { ... }
    * </listing>
    *
    * <p>If the token-referenced properties (i.e., foo and bar in the examples above) are
    * bindable, whenever any of these properties change, the [Injectable] property will be
    * re-injected if necessary.
    *
    * <p>Apart from the parameters described above, Injectable tag also accepts a "policy"
    * parameter which indicates the creation or lifetime policy for the object to be
    * injected. It can be used as a substitute or to override the value specified using the
    * LifetimePolicy tag on the class definition (of the property type). The parameter can
    * take on these values: keepalive, createnew, lookuponly. <code>lookuponly</code>
    * instructs the injection manager to inject only existing objects, i.e. no new objects
    * are created in the case when no existing objects satisfy the given spec. "keepalive"
    * essentially means "shared", i.e., the newly created instance will be kept around by
    * the framework and injected at other places if needed. "createnew" on the other hand
    * implies "exclusive use", i.e., the newly created instance is injected only once and
    * not kept around. The following example shows how to specify the parameter:
    *
    * <listing version="3.0">
    * ...
    * [Injectable( ..., policy="createnew", ... )]
    * public var anInjectableProperty:SomePropertyType;
    * </listing>
    *
    * <p>If the type of the [Injectable] property (as declared by the property itself, or by
    * an ArrayElementType metatag) is not compatible with the type of the objects requested
    * by the [Injectable] tag, the InjectionManager implicitly employs the global instane of
    * TypeTransformManager to convert the objects to the target-expected type.
    *
    * <p>A property declared as [Injectable] on an injection target (i.e., an object in
    * one of the consumer registries) gets injected with the default implementation of the
    * declared property type. The default implementation for a class is the class itself;
    * that for an interface is the class declared through the [DefaultImplementation] tag
    * (see ProxyRegistry for usage). An already existing instance of the default
    * implementation type is used if an instance is known to the injection manager;
    * otherwise a new instance is created.
    *
    * <p>When creating a new object to inject, if the newly created object doesn't satisfy
    * the property specification given in the tag, the injection manager tries to bring the
    * object into conformance by setting the requisite properties on it with the specified
    * values. If this fails, no objects are injected at the injectable site. Note that nested
    * property names are supported by this use-case just as they're supported for the pure
    * lookup use-case.
    *
    * <p>The [Injectable] metatag allows an optional 'selectionContext' parameter that can be
    * used by the client to provide some arbitrary data representing the context in which
    * objects are being looked up. This data typically contains properties of the client
    * object (which is the consumer of the queried objects), as opposed to the properties of
    * the queried objects. This context information can be used by an object provider or
    * directory to filter the set of applicable objects.
    *
    * <p>If selectionContext is set to the value 'NONE', InjectionManager uses a null or
    * empty selection context. The same holds for the simple tag case (i.e., when
    * [Injectable] is used without any parameters).
    *
    * <p>If selectionContext is set to the value 'CONTEXT_PROPERTIES' or if it's skipped
    * altogether (except for the simple tag case), InjectionManager uses the [Context]
    * properties of the injection target component to materialize the selection context
    * object. Only those context properties are included that have their
    * <code>definesInjectionContext</code> specified as 'true'.
    *
    * <p>An [Injectable] property is left untouched if the computed new value for it is
    * <i>equivalent</i> to its current value. The value is deemed equivalent if it is equal
    * or has the same <code>uid</code>, assuming both values implement IUID. If the two
    * values are ordered collections, then equivalency is determined using their elements.
    *
    * <p>The [Injectable] metatag allows an optional 'finalized' parameter that can be used
    * to provide a callback on the target object to be invoked every time the property is
    * evaluated for injection, even if it's eventually left untouched due to the property
    * having the same value as the newly computed value.
    *
    * <p>The [Injectable] metatag also allows an optional 'skipped' parameter that can be
    * used to provide a callback on the target object to be invoked every time the property
    * is evaluated for injection, but is skipped (i.e., left untouched) due to the property
    * having the same value as the newly computed value, or due to no matching objects being
    * found.
    *
    * <p>When the injectable tag is used in the form [Injectable("foo")], the equivalent
    * ObjectFilter (which is used to look up the objects to inject using the registered
    * object directories) contains the tag argument value ('foo' in the above example) as a
    * meta-property filter whose key is the empty string.
    *
    * <p>If the injection spec of a property <i>x</i> depends on another property <i>y</i>,
    * and <i>y</i> changes while <i>x</i> is being injected (i.e., its setter is under
    * invocation by this service), then the change in <i>y</i> is ignored, i.e. it does not
    * trigger re-injection of <i>x</i> (in order to avoid indefinite recursion or potentially
    * unnecessary work).
    *
    * <p>Any errors reported by the object directories while retrieving the injectable
    * objects are reported to the injection target component using the component's
    * generic error handler method (i.e., a method that is tagged as [ErrorHandler]).
    *
    * <p>An object can opt-out or be excluded from injection by raising a
    * ServiceApplicabilityUpdateEvent such that the specified service id is "Injection". This
    * event can be raised either on some object that's supposed to be on the associated event
    * bus or it can be raised on the Application.
    *
    * @see TypeTransformManager
    * @see ProxyRegistry
    */
   public class InjectionManager
   {
      private static const TAGPARAM_INSTANCEID_KEY:String =
         Constants.TAGPARAM_INSTANCEID_KEY;
      private static const TAGPARAM_POLICY_KEY:String =
         Constants.TAGPARAM_POLICY_KEY;
      private static const TAGPARAM_SELECTION_CONTEXT:String = "selectionContext";
      /**
       * If selectionContext is specified as 'CONTEXT_PROPERTIES', it is be treated as a
       * special value, indicating that the [Context] properties should be used to
       * materialize the selectionContext.
       */
      private static const TAGPARAM_OPTION_USE_CONTEXT_PROPERTIES:String =
         "CONTEXT_PROPERTIES";
      /**
       * If selectionContext is specified as 'NONE', it is be treated as a special value,
       * indicating the use of a null or empty selectionContext.
       */
      private static const TAGPARAM_OPTION_USE_NULL_CONTEXT:String = "NONE";

      /**
       * The tag parameter key to signify that the tagged property should be used as part of
       * the selectionContext.
       */
      private static const TAGPARAM_DEFINES_INJECTION_CONTEXT:String =
         "definesInjectionContext";

      /**
       * This option during the injection process indicates that the matching objects should
       * be appended to the current value of the injectable property (as opposed to
       * overwriting that value). In other words, if this option is not specified, the
       * property will be reset before the (first) value is injected (unless there's no
       * reason to touch the value because no re-evaluation trigger is present).
       */
      private static const APPEND_ONLY:int = 1 << 0;

      /**
       * This option during the injection process indicates that the target injectable
       * property should be overwritten even if there's no reason to re-evaluate the matching
       * object set (e.g., even if there's no change in the spec), except when a certain
       * property is expressly requested to be skipped.
       */
      private static const FORCE_RESET:int = 1 << 1;

      private static var _instance:InjectionManager;
      private var _eventBusRef:SharedReference = SharedReference.newReference(EventBus);
      private var _directories:Array/*IObjectDirectory*/ = [];
      private var _objsUnderWatch:Dictionary = new Dictionary( true );
      private var _svcApplicabilityRegistry:ServiceApplicabilityRegistry;
      /** injection-target to InjectionTargetInfo map */
      private var _activeInjectionTargets:Dictionary = new Dictionary( true );
      private var _fallbackDirectory:IObjectDirectory;

      /**
       * The id of this (type of) service, which can be used in a
       * ServiceApplicabilityUpdateEvent.
       *
       * @see com.ease.flexutil.ServiceApplicabilityUpdateEvent#serviceId
       */
      public static const SERVICE_ID:String = "Injection";

      /**
       * A global instance of this class. This is the instance that would typically be used
       * by an application.
       *
       * @return
       *    An instance of InjectionManager for global use.
       */
      public static function get instance():InjectionManager {
         if (_instance == null) {
            _instance = new InjectionManager( EventBus.instance );
            _instance.globalInstanceInit();
         }
         return _instance;
      }

      /** @private */
      public static function isInjectableCollectionType( type:Class ):Boolean {
         var isInjectableCollectionType:Boolean =
            ReflectionUtil.isTypeCompatibleWith( type, Array ) ||
            ReflectionUtil.isTypeCompatibleWith( type, IList );
         return isInjectableCollectionType;
      }

      /**
       * Initializes an instance of this class.
       *
       * @param eventBus
       *    An optional EventBus instance associated with this service, used for the
       *    purposes of listening to events like RelationshipUpdateEvent or
       *    ServiceApplicabilityUpdateEvent.
       */
      public function InjectionManager( eventBus:EventBus = null ) {
         this.eventBus = eventBus;
         _fallbackDirectory = new FactoryBasedObjectDirectory( this.eventBus );
         _svcApplicabilityRegistry = new ServiceApplicabilityRegistry();
         _svcApplicabilityRegistry.associatedService = this;
         if (eventBus == null) {
            return;
         }
         // We listen for service requests on the associated event bus. These events allow
         // the client to request enable, disable or re-apply of the "Injection" service
         // selectively on individual objects.
         eventBus.addEventListener( ServiceApplicabilityUpdateEvent.SERVICE_ENABLE_REQUESTED,
            onServiceRequestEvent );
         eventBus.addEventListener( ServiceApplicabilityUpdateEvent.SERVICE_DISABLE_REQUESTED,
            onServiceRequestEvent );
         eventBus.addEventListener( ServiceApplicabilityUpdateEvent.SERVICE_PROCESSING_REQUESTED,
            onServiceRequestEvent );
      }

      /**
       * Adds the given directory as a provider of injection (source) objects.
       */
      public final function addProvider( directory:IObjectDirectory ):void {
         if (_directories.indexOf( directory ) >= 0) {
            return;
         }
         _directories.push( directory );
      }

      /**
       * Removes the given directory as a provider of injection (source) objects.
       */
      public final function removeProvider( directory:IObjectDirectory ):void {
         MixedUtil.removeItemFromArray( directory, _directories );
      }

      /**
       * Adds the given registry as a notifier of injectable (target) objects.
       */
      public final function addConsumer( registry:IObjectModelSource ):void {
         registry.addEventListener( ObjectEvent.OBJECT_ADDED,
            onInjectableObjectAddedRemoved );
         registry.addEventListener( ObjectEvent.OBJECT_REMOVED,
            onInjectableObjectAddedRemoved );
      }

      /**
       * Removes the given registry as a notifier of injectable (target) objects.
       */
      public final function removeConsumer( registry:IObjectModelSource ):void {
         registry.removeEventListener( ObjectEvent.OBJECT_ADDED,
            onInjectableObjectAddedRemoved );
         registry.removeEventListener( ObjectEvent.OBJECT_REMOVED,
            onInjectableObjectAddedRemoved );
      }

      /**
       * Removes the given objects as injection targets and from the injection points where
       * they might have been injected before. Note that the operation can be O(n+p), where
       * n is the total number of objects being tracked by the injection manager and p is
       * the total number of injection points these objects have.
       */
      public final function removeObjects( objects:Array ):void {
         var objsToRemove:Set = Set.fromArray( objects );
         for (var target:Object in _activeInjectionTargets) {
            var targetInfo:InjectionTargetInfo = _activeInjectionTargets[target];
            for each (var pi:TaggedPropertyInfo in targetInfo.staticallyInjectedProps) {
               removeObjectsFromPropertyValue( target, pi, objsToRemove );
            }
            for each (var ipi:InjectablePropertyInfo in
               targetInfo.propsToReinjectOnChanges) {
                  pi = ipi.propertyInfo;
                  removeObjectsFromPropertyValue( target, pi, objsToRemove );
               }
         }
         for each (var object:* in objects) {
            removeTarget( object );
         }
      }

      /**
       * Injects the objects provided by the given directories into the given injection
       * targets. If the given target array is undefined, it defaults to the set of targets
       * that are currently being tracked by this injection manager. If the given directory
       * array is undefined, it defaults to the directories currently registered with this
       * injection manager.
       *
       * <p>The method doesn't reset the current values of the target injectable properties
       * (but rather appends to them in the collection case).
       *
       * <p>The given target objects are also added to the set of active targets, and
       * monitored for any property changes that are supposed to trigger re-injection.
       *
       * @param targets
       *    The target objects for this injection request.
       *
       * @param directories
       *    The supplier of objects for this injection request.
       */
      public final function
         injectTargetsUsingProviders( targets:Array,
                                      directories:Array/*IObjectDirectory*/ ):void {
         var dirs:Array/*IObjectDirectory*/;
         if (directories != null) {
            dirs = directories;
         } else {
            dirs = _directories;
         }
         if (targets == null) {
            targets = MixedUtil.getKeys( _activeInjectionTargets );
         }
         // Since this is an incremental job, i.e. the directories provided here are not
         // necessarily the entire set of directories available out there, we'd want to be
         // safe and not reset the current values of the properties, hence APPEND_ONLY.
         var options:int = APPEND_ONLY;
         for each (var target:Object in targets) {
            setAllInjectablePropertiesOn( target, options, dirs );
         }
      }

      private function globalInstanceInit():void {
         var app:EventDispatcher = FlexGlobals.topLevelApplication as EventDispatcher;
         // The global instance of this class listens for the service enable/disable request
         // events on the Application as well, i.e., not just on the associated event bus (so
         // that even those objects can raise them which are not already on some event bus).
         // Note that we don't (need to) use weak-referencing here (i.e., when adding the
         // listeners) because this global instance is for the lifetime of the app.
         // Also note that multiple notifications (i.e., from the bus as well as from the
         // system manager, if that ever happens) are fine since object add/remove in
         // this class is idempotent.
         app.addEventListener(
            ServiceApplicabilityUpdateEvent.SERVICE_ENABLE_REQUESTED,
            onServiceRequestEvent );
         app.addEventListener(
            ServiceApplicabilityUpdateEvent.SERVICE_DISABLE_REQUESTED,
            onServiceRequestEvent );
         app.addEventListener(
            ServiceApplicabilityUpdateEvent.SERVICE_PROCESSING_REQUESTED,
            onServiceRequestEvent );
      }

      private function onServiceRequestEvent( ev:ServiceApplicabilityUpdateEvent ):void {
         if (ev.serviceId != SERVICE_ID) {
            return;
         }
         var object:Object = ev.object;

         // A one-off (re)processing request.
         if (ev.type == ServiceApplicabilityUpdateEvent.SERVICE_PROCESSING_REQUESTED) {
            reprocessTargets( object );
            return;
         }
         // Enable/disable request.

         if (! MixedUtil.isIterableCollection( object )) {
            enableDisableServiceFor( object, ev );
            return;
         }
         for each (var item:Object in object) {
            enableDisableServiceFor( item, ev );
         }
      }

      private function enableDisableServiceFor( object:Object,
                                                ev:ServiceApplicabilityUpdateEvent ):void {
         if (object == null) {
            return;
         }
         if (_svcApplicabilityRegistry.isPermanentlyDisabled( object ) &&
            ev.type == ServiceApplicabilityUpdateEvent.SERVICE_ENABLE_REQUESTED) {
            return;
         }
         _svcApplicabilityRegistry.applyChange( object, ev );
         var opCode:String = eventIdToOpCode( ev.type );
         addRemoveObject( object, opCode );
      }

      private static function eventIdToOpCode( eventTypeId:String ):String {
         switch (eventTypeId) {
            case ServiceApplicabilityUpdateEvent.SERVICE_ENABLE_REQUESTED:
               return ObjectEvent.OBJECT_ADDED;
            case ServiceApplicabilityUpdateEvent.SERVICE_DISABLE_REQUESTED:
               return ObjectEvent.OBJECT_REMOVED;
            default:
               return null;
         }
      }

      /**
       * Adds the given object to the set of active targets, and processes its injectable
       * properties right away.
       */
      public final function addTarget( object:Object ):void {
         if (! isAutoInjectionEnabledForTarget( object )) {
            return;
         }

         // See if it's a view or a mediator, and whether the view in question has been
         // "initialized" or not; if not, give it a chance to complete initialization before
         // causing its properties to be injected.
         var view:UIComponent;
         if (object is UIComponent) {
            view = UIComponent( object );
         } else {
            view = ViewRegistry.getViewFromMediator( object ) as UIComponent;
         }
         if (view != null && ! view.initialized) {
            var viewInitCallback:Function = function( ev:Event = null ):void {
               view.removeEventListener( FlexEvent.CREATION_COMPLETE, viewInitCallback );
               addTarget( object ); // add the object (which can be either view or mediator)
            }
            view.addEventListener( FlexEvent.CREATION_COMPLETE, viewInitCallback );
            return;
         }

         setAllInjectablePropertiesOn( object );
      }

      /**
       * Removes the given object from the set of active targets, stops the in-progress
       * injection jobs on it and stops monitoring its properties.
       *
       * <p>Note that it does not de-inject whatever might have been injected into the target
       * before.
       */
      public final function removeTarget( target:Object ):void {
         var targetInfo:InjectionTargetInfo = _activeInjectionTargets[target];
         if (targetInfo != null) {
            // stop whatever injection jobs may be in progress
            targetInfo.resetInjectionTickets();
            delete _activeInjectionTargets[target];
         }
         var objMonitor:ObjectMonitor = _objsUnderWatch[target];
         if (objMonitor != null) {
            objMonitor.stop( target as IEventDispatcher );
            delete _objsUnderWatch[target];
         }
      }

      /** The given object can either be an individual target or a collection. */
      private function reprocessTargets( object:Object ):void {
         if (MixedUtil.isIterableCollection( object )) {
            for each (var item:Object in object) {
               reprocessTargets( item );
            }
            return;
         }
         if (_activeInjectionTargets[object] != null) {
            // Just re-inject the properties if the object is already known as a target.
            // Force-inject so that no optimization would kick-in that could bypass the
            // re-evaluation.
            setAllInjectablePropertiesOn( object, FORCE_RESET );
            return;
         }
         // The object is not registered with this service as a target.

         addTarget( object );
         removeTarget( object );
      }

      private function onInjectableObjectAddedRemoved( ev:ObjectEvent ):void {
         var object:Object = ev.object;
         var opCode:String = ev.type;
         addRemoveObject( object, opCode );
      }

      /**
       * Adds or removes the given object for property injection purposes.
       *
       * @param opCode
       *     Use ObjectEvent.OBJECT_ADDED for add, ObjectEvent.OBJECT_REMOVED for remove.
       */
      private function addRemoveObject( object:Object, opCode:String ):void {
         if (opCode == ObjectEvent.OBJECT_REMOVED) {
            removeTarget( object );
            return;
         }
         if (opCode == ObjectEvent.OBJECT_ADDED) {
            addTarget( object );
            return;
         }
      }

      /**
       * Returns a flag indicating whether injection of properties is enabled on the given
       * object.
       */
      private function isAutoInjectionEnabledForTarget( object:Object ):Boolean {
         return _svcApplicabilityRegistry.isServiceEnabledFor( object );
      }

      /**
       * Returns a collection with the given set of objects removed. The removal is done
       * in-place, i.e. the returned collection object is the same as the input collection
       * object, if the collection type is bindable (i.e., IList). Else a new instance is
       * returned (i.e., for Array).
       *
       * <p>The removal also takes into account possible type transformations that might
       * have been done such that the transform result was inserted into the collection
       * rather than the original.
       */
      private static function removeGivenSetFromCollection( collection:Object,
                                                            objsToRemove:Set ):* {
         if (collection is Array) {
            var newCollection:Array = [];
            for each (var object:* in collection) {
               if (objsToRemove.contains( object )) {
                  continue;
               }
               var orig:* = PropertyInjectionJob.originalByTransformed[object];
               if (orig && objsToRemove.contains( orig )) {
                  delete PropertyInjectionJob.originalByTransformed[object];
                  continue;
               }
               newCollection.push( object );
            }
            if (collection.length == newCollection.length) {
               return collection;
            }
            return newCollection;
         }
         if (collection is IList) {
            var list:IList = IList( collection );
            for (var i:int = list.length - 1; i >= 0; i--) {
               object = list.getItemAt( i );
               if (objsToRemove.contains( object )) {
                  list.removeItemAt( i );
                  continue;
               }
               orig = PropertyInjectionJob.originalByTransformed[object];
               if (orig && objsToRemove.contains( orig )) {
                  list.removeItemAt( i );
                  delete PropertyInjectionJob.originalByTransformed[object];
               }
            }
         }
         return collection;
      }

      private static function
         isPropertyValueGetableAndNonNull( propertyProvider:*, pi:TaggedPropertyInfo ):Boolean {
         if (pi.access == "writeonly") {
            return false;
         }
         var propValue:* = propertyProvider[pi.name];
         if (propValue) {
            return true;
         }
         return false;
      }

      private static function
         removeObjectsFromPropertyValue( propertyProvider:*, pi:TaggedPropertyInfo,
                                         objsToRemove:Set ):void {
         if (pi.access == ReflectionUtil.ACCESS_WRITEONLY) {
            // We do the removal by looking up the property value, and in this case we can't
            // do that.
            return;
         }
         var propName:String = pi.name;
         var propValue:* = propertyProvider[propName];
         if (! propValue) {
            return;
         }
         if (objsToRemove.contains( propValue )) {
            propertyProvider[propName] = null;
            return;
         }
         var orig:* = PropertyInjectionJob.originalByTransformed[propValue];
         if (orig && objsToRemove.contains( orig )) {
            propertyProvider[propName] = null;
            delete PropertyInjectionJob.originalByTransformed[propValue];
            return;
         }
         if (! isInjectableCollectionType( pi.propertyType )) {
            return;
         }
         var newCollection:* = removeGivenSetFromCollection( propValue, objsToRemove );
         if (newCollection != propValue) {
            propertyProvider[propName] = newCollection;
         }
      }

      /**
       * "Dynamically Injectable" (DI) properties are those whose injection spec is
       * parametrized, e.g. on some property of the object, such that a change in the
       * object's properties would necessitate reinjection of these DI properties.
       */
      private function setDynamicallyInjectablePropertiesOn(
         object:Object, propsToExclude:Set/*String*/ = null,
         directories:Array/*IObjectDirectory*/ = null,
         options:int = 0 ):void {
         if (directories == null) {
            directories = _directories;
         }
         var injectionJobs:Array = createInjectionJobsForDynamicallyInjectableProps(
            object, directories, options, propsToExclude );
         for each (var injectionJob:PropertyInjectionJob in injectionJobs) {
            injectionJob.start();
         }
      }

      /**
       * @param options
       *    Instructions for how injection should be performed; possible values include
       *    APPEND_ONLY and FORCE_RESET.
       */
      private function setAllInjectablePropertiesOn(
         object:Object, options:int = 0,
         directories:Array/*IObjectDirectory*/ = null ):void {
         var appendOnly:Boolean = (options & APPEND_ONLY) != 0;
         var mustReset:Boolean = (options & FORCE_RESET) != 0;
         if (directories == null) {
            directories = _directories;
         }
         var injectionJobs:Array/*PropertyInjectionJob*/;
         var targetInfo:InjectionTargetInfo = _activeInjectionTargets[object];
         if (targetInfo != null) {
            if (!appendOnly || mustReset) {
               // we're injecting from scratch, so cancel any pending jobs
               targetInfo.resetStaticInjectionTicket();
            }
            injectionJobs =
               createInjectionJobsForPreExistingTarget( object, directories, options );
         } else {
            injectionJobs = createInjectionJobsForNewTarget( object, directories,
               appendOnly );
         }
         if (injectionJobs.length == 0) {
            return;
         }
         for each (var injectionJob:PropertyInjectionJob in injectionJobs) {
            injectionJob.start();
         }
      }

      /**
       * Creates injection jobs for the given object as the target, adds it to the collection
       * of active targets and starts monitoring the object for any property changes that are
       * supposed to trigger re-injection in the future.
       */
      private function createInjectionJobsForNewTarget(
         object:*, directories:Array/*IObjectDirectory*/,
         appendOnly:Boolean ):Array/*PropertyInjectionJob*/ {
         var propsInfo:Array =
            ReflectionUtil.getAllKeyValuesForTaggedProperties( object, "Injectable" );
         if (propsInfo.length == 0) {
            return [];
         }
         var targetInfo:InjectionTargetInfo = new InjectionTargetInfo();
         _activeInjectionTargets[object] = targetInfo;
         var propsToMonitor:Set/*String*/ = new Set();
         var propsToReinjectOnChanges:Dictionary = new Dictionary();
         var injectionJobs:Array/*PropertyInjectionJob*/ = [];
         for each (var pi:TaggedPropertyInfo in propsInfo) {
            if (pi.access == ReflectionUtil.ACCESS_WRITEONLY) {
               // Injection into "writeonly" properties is bad since it can't be undone
               // correctly.
               var msg:String = StringUtil.substitute(
                  "Property {0} on {1} declared as Injectable: writeonly properties " +
                  "are not supported for injection", pi.name, object );
               Diagnostics.assert( false, msg );
            }
            var injPropInfo:InjectablePropertyInfo =
               specForInjectionOfPropOnTarget( pi, object, propsToMonitor );
            if (injPropInfo.spec.isPropertyToBeReinjectedOnTargetChanges) {
               propsToReinjectOnChanges[pi.name] = injPropInfo;
            } else {
               targetInfo.staticallyInjectedProps.push( pi );
            }
            var injectionJob:PropertyInjectionJob =
               newInjectionJob( object, injPropInfo, directories, appendOnly );
            if (injectionJob != null) {
               injectionJobs.push( injectionJob );
            }
         }
         // It is critical that we start monitoring before starting the injection jobs
         // (which should happen outside this method) because injection may cause some
         // properties to change that we're supposed to monitor and respond to.
         startMonitoring( object as IEventDispatcher, propsToMonitor,
            propsToReinjectOnChanges );
         return injectionJobs;
      }

      private function createInjectionJobsForPreExistingTarget(
         object:*, directories:Array/*IObjectDirectory*/,
         options:int = 0 ):Array/*PropertyInjectionJob*/ {
         var appendOnly:Boolean = (options & APPEND_ONLY) != 0;
         var targetInfo:InjectionTargetInfo = _activeInjectionTargets[object];
         var injectionJobs:Array/*PropertyInjectionJob*/ = [];
         for each (var pi:TaggedPropertyInfo in targetInfo.staticallyInjectedProps) {
            var injPropInfo:InjectablePropertyInfo =
               specForInjectionOfPropOnTarget( pi, object );
            var injectionJob:PropertyInjectionJob =
               newInjectionJob( object, injPropInfo, directories, appendOnly );
            if (injectionJob != null) {
               injectionJobs.push( injectionJob );
            }
         }
         var dynamicPropInjectionJobs:Array/*PropertyInjectionJob*/ =
            createInjectionJobsForDynamicallyInjectableProps(
               object, directories, options );
         injectionJobs = injectionJobs.concat( dynamicPropInjectionJobs );
         return injectionJobs;
      }

      private function createInjectionJobsForDynamicallyInjectableProps(
         object:Object, directories:Array/*IObjectDirectory*/, options:int = 0,
         propsToExclude:Set/*String*/ = null ):Array/*PropertyInjectionJob*/ {
         var targetInfo:InjectionTargetInfo = _activeInjectionTargets[object];
         var propsToConsider:Dictionary/*(String, InjectablePropertyInfo)*/ =
            targetInfo.propsToReinjectOnChanges;
         if (propsToConsider == null) {
            return [];
         }
         var appendOnly:Boolean = (options & APPEND_ONLY) != 0;
         var mustReset:Boolean = (options & FORCE_RESET) != 0;
         var injectionJobs:Array/*PropertyInjectionJob*/ = [];
         for each (var ipi:InjectablePropertyInfo in propsToConsider) {
            var propName:String = ipi.propertyInfo.name;
            if (propsToExclude != null && propsToExclude.contains( propName )) {
               continue;
            }
            var spec:PropertyInjectionObjectSpec = ipi.spec;
            var isSpecModified:Boolean =
               spec.reassessValueTokensBasedOnObjectProperties( object );
            // Here we check whether we should skip the property from injection (e.g., if the
            // property already has a legit value).
            // If we're not allowed to overwrite the property, that means it's an incremental
            // injection. We consider skipping only if it's a full reset of the property.
            var canResetProperty:Boolean = !appendOnly;
            if (canResetProperty) {
               if (!mustReset && !isSpecModified &&
                  isPropertyValueGetableAndNonNull( object, ipi.propertyInfo )) {
                  // The property (still) has a non-null value, the spec hasn't changed since
                  // the last time we computed it, and we're not forced to reset the property,
                  // so skip it.
                  continue;
               }
            }
            var injectionJob:PropertyInjectionJob =
               newInjectionJob( object, ipi, directories, appendOnly );
            if (injectionJob != null) {
               injectionJobs.push( injectionJob );
            }
         }
         return injectionJobs;
      }

      /**
       * Computes a spec for the objects that will be injected into the given object as the
       * value of the given property.
       *
       * @param propsToMonitor
       *    An out param containing info about the properties of the given object that should
       *    be monitored to trigger reinjection.
       */
      private function specForInjectionOfPropOnTarget(
         pi:TaggedPropertyInfo, target:Object,
         propsToMonitor:Set/*String*/ = null ):InjectablePropertyInfo {
         var spec:PropertyInjectionObjectSpec = new PropertyInjectionObjectSpec();
         computeFiltersForInjectionOfPropertyOnTarget( pi, target, spec, propsToMonitor );
         var propType:Class = pi.propertyType;
         var isPropertyInjectableCollectionType:Boolean =
            isInjectableCollectionType( propType );
         if (isPropertyInjectableCollectionType) {
            spec.targetExpectedArrayElementType =
               ReflectionUtil.getTaggedArrayElementType( pi.name, target );
            if (spec.targetExpectedArrayElementType == null) {
               spec.targetExpectedArrayElementType = Object;
            }
         }
         // If no type is explicitly supplied, we try to infer it from the target property
         // type. This is meaningful only if the spec'ed objects themselves would be
         // inserted (as opposed to some property thereof), hence the sourceProperty check.
         if (! spec.type && ! spec.sourceProperty) {
            if (isPropertyInjectableCollectionType) {
               spec.type = spec.targetExpectedArrayElementType;
            } else {
               spec.type = propType;
            }
         }
         if (spec.type == null || spec.type == Object) {
            Diagnostics.assert(
               false, "No valid type specified on the [Injectable] property " + pi.name );
         }
         if (spec.type && spec.sourceProperty) {
            spec.specTargetedValueType =
               ReflectionUtil.getDeclaredPropertyType( spec.sourceProperty, spec.type );
         } else {
            spec.specTargetedValueType = spec.type;
         }
         var injPropInfo:InjectablePropertyInfo = new InjectablePropertyInfo( pi, spec );
         return injPropInfo;
      }

      /**
       * Computes the property filter and meta-property filter parts of the spec for the
       * objects that will be injected into the given target as the value of the given
       * property.
       *
       * @param propsToMonitor
       *    An out param containing info about the properties of the given object that
       *    should be monitored to trigger reinjection.
       */
      private function
         computeFiltersForInjectionOfPropertyOnTarget( pi:TaggedPropertyInfo, target:Object,
                                                       spec:PropertyInjectionObjectSpec,
                                                       propsToMonitor:Set/*String*/ ):void {
         // "property" here refers to a property on the object that will be injected into
         // the given object
         var propNameForFilter:String;
         var propName:String;
         var isMetaProperty:Boolean = false;
         // The default behavior is to use the context properties as the selection context,
         // except for the simple [Injectable] case.
         var useCtxPropsAsSelectionCtx:Boolean = pi.tagArgs.length > 0;
         for each (var arg:TagArgReflectionInfo in pi.tagArgs) {
            var argKey:String = arg.key;
            var argVal:String = arg.value;
            var propValToken:String = null;
            var propValForFilter:Object = null;
            switch (argKey) {
               case "type":
                  spec.type = ReflectionUtil.getDefinition( argVal, target );
                  break;
               case TAGPARAM_POLICY_KEY:
                  spec.policy = argVal; // passthrough value
                  break;
               case TAGPARAM_SELECTION_CONTEXT:
                  if (argVal == TAGPARAM_OPTION_USE_NULL_CONTEXT) {
                     useCtxPropsAsSelectionCtx = false;
                  } else if (argVal != TAGPARAM_OPTION_USE_CONTEXT_PROPERTIES) {
                     useCtxPropsAsSelectionCtx = false;
                     propValForFilter = ReflectionUtil.resolveTagValue( argVal, target );
                     if (propValForFilter != argVal) {
                        propValToken = argVal;
                     }
                     spec.setSelectionContext( propValForFilter, propValToken );
                  }
                  break;
               case "selectProperty":
                  propNameForFilter = argVal;
                  isMetaProperty = false;
                  break;
               case "injectProperty":
                  spec.sourceProperty = argVal;
                  break;
               case "selectMetaProperty":
                  propNameForFilter = argVal;
                  isMetaProperty = true;
                  break;
               case "":
                  // For usage of the form: [Injectable("foo")]
                  propNameForFilter = "";
                  isMetaProperty = true;
                  // fall through to the "value" case
               case "value":
                  addMonitorableProps_ForValueToken( argVal, target, spec, propsToMonitor );
                  if (isMetaProperty) {
                     spec.addMetaPropertyFilter( propNameForFilter, argVal, target );
                  } else {
                     spec.addPropertyFilter( propNameForFilter, argVal, target );
                  }
                  break;
               case "filter":
                  addMonitorableProps_ForValueToken( argVal, target, spec, propsToMonitor );
                  spec.addFiltersUsingPredicateExpr( argVal, target );
                  break;
               case "finalized":
                  propName = MixedUtil.getSingleEvaluatableToken( argVal );
                  if (propName == null) {
                     Diagnostics.assert( false, "'finalized' has bad value" );
                  } else {
                     spec.postInjectionFinalizeCallbackName = propName;
                  }
                  break;
               case "skipped":
                  propName = MixedUtil.getSingleEvaluatableToken( argVal );
                  if (propName == null) {
                     Diagnostics.assert( false, "'skipped' has bad value" );
                  } else {
                     spec.postInjectionSkipCallbackName = propName;
                  }
                  break;
               default:
                  Diagnostics.assert( false,
                     "Invalid [Injectable] tag parameter name: " + argKey );
                  break;
            }
         }
         if (useCtxPropsAsSelectionCtx) {
            applyContextPropsAsInjectionContext( target, spec, propsToMonitor );
         }
      }

      private static function addMonitorableProps_ForValueToken(
         valueToken:String, target:Object, spec:PropertyInjectionObjectSpec,
         propsToMonitor:Set/*String*/ ):void {
         var propTokens:Array =
            ReflectionUtil.getPropertyTokensFromExpr( valueToken, target );
         if (propTokens.length == 0) {
            return;
         }
         // the value has evaluation tokens that are properties on the 'target' object

         spec.isPropertyToBeReinjectedOnTargetChanges = true;
         if (propsToMonitor == null) {
            return;
         }
         var newPropsToMonitor:Array/*String*/ = MixedUtil.getFirstWords( propTokens, "." );
         propsToMonitor.addCollection( newPropsToMonitor );
      }

      /**
       * Uses the [Context] properties to materialize the selectionContext. Also, if any such
       * props are found, then spec's isPropertyToBeReinjectedOnTargetChanges is set to true.
       * Also adds the Context properties to propsToMonitor.
       */
      private static function
         applyContextPropsAsInjectionContext( target:Object,
                                              spec:PropertyInjectionObjectSpec,
                                              propsToMonitor:Set ):void {
         var ctxProps:Array/*TaggedPropertyInfo*/ =
            ReflectionUtil.getFilteredTaggedProperties(
               target, Constants.TAG_CONTEXT, TAGPARAM_DEFINES_INJECTION_CONTEXT,
               "true", "name" );
         if (ctxProps.length == 0) {
            return;
         }
         spec.isPropertyToBeReinjectedOnTargetChanges = true;
         var context:ObjectProjection = new ObjectProjection();
         for each (var pi:TaggedPropertyInfo in ctxProps) {
            var projectedName:String = pi.name;
            if (pi.tagValue != null) {
               projectedName = String( pi.tagValue );
            }
            context.addProjection( projectedName, pi.name );
            if (pi.access != ReflectionUtil.ACCESS_READONLY) {
               // Only writable properties can be monitored.
               propsToMonitor.add( pi.name );
            }
         }
         context.evaluate( target );
         spec.setSelectionContext( context );
      }

      private function startMonitoring( object:IEventDispatcher, propsToMonitor:Set,
                                        propsToReinjectOnChanges:Dictionary ):void {
         if (object == null || propsToMonitor == null || propsToMonitor.length == 0) {
            // nothing to monitor, really
            return;
         }
         if (_objsUnderWatch[object] != null) {
            // already being monitored
            return;
         }
         var targetInfo:InjectionTargetInfo = _activeInjectionTargets[object];
         targetInfo.propsToReinjectOnChanges = propsToReinjectOnChanges;
         // setup the object for monitoring
         var objMonitor:ObjectMonitor = new ObjectMonitor();
         objMonitor.propsToMonitor = propsToMonitor;
         objMonitor.propertyInjector = setDynamicallyInjectablePropertiesOn;
         _objsUnderWatch[object] = objMonitor;
         objMonitor.start( object );
      }

      /**
       * Returns true if the target property's current value (treated as a scalar, not
       * collection) matches the object spec.
       */
      private function currentPropValUnnecessitatesInjection(
         target:Object, ipi:InjectablePropertyInfo,
         directories:Array/*IObjectDirectory*/ ):Boolean {
         var pi:TaggedPropertyInfo = ipi.propertyInfo;
         if (isInjectableCollectionType( pi.propertyType )) {
            return false;
         }
         if (pi.access == ReflectionUtil.ACCESS_WRITEONLY) {
            // always re-inject properties that can't be read
            return false;
         }
         var value:* = target[pi.name];
         if (value == null) {
            return false;
         }
         var propInjSpec:PropertyInjectionObjectSpec = ipi.spec;
         var passesFilter:Boolean =
            propInjSpec.passesFilterUsingDirectories( value, directories );
         return passesFilter;
      }

      private function newInjectionJob(
         target:Object, ipi:InjectablePropertyInfo, directories:Array/*IObjectDirectory*/,
         appendOnly:Boolean ):PropertyInjectionJob {
         var pi:TaggedPropertyInfo = ipi.propertyInfo;
         var propInjSpec:PropertyInjectionObjectSpec = ipi.spec;
         if (ipi.dynamicInjectionTicket.isPropertySettingInProgress) {
            // The property is currerntly being set (i.e., its setter is on the callstack),
            // which means the setter must have triggered re-injection on the target, so we
            // skip this property (to avoid recursive or duplicate work).
            return null;
         }
         if (currentPropValUnnecessitatesInjection( target, ipi, directories )) {
            return null;
         }
         var injectionJob:PropertyInjectionJob = new PropertyInjectionJob(
            target, pi, propInjSpec, directories, appendOnly, _fallbackDirectory );
         // We use the injection ticket to expire ongoing injection jobs upon a canceling
         // event. Examples of such events include restarting the injection from scratch
         // upon some property change on the object.
         if (propInjSpec.isPropertyToBeReinjectedOnTargetChanges) {
            if (!appendOnly) {
               // we're injecting the property from scratch, so cancel any pending jobs
               ipi.resetDynamicInjectionTicket();
            }
            injectionJob.ticket = ipi.dynamicInjectionTicket;
         } else {
            var targetInfo:InjectionTargetInfo = _activeInjectionTargets[target];
            injectionJob.ticket = targetInfo.staticInjectionTicket;
         }
         return injectionJob;
      }

      //------------------------------------------------------------------------------------
      // Instance properties
      // The property provider used avoids directly referencing the global instance.
      // This is done to simplify the object graph for Memory Profiler.

      private function get eventBus():EventBus {
         return _eventBusRef.object;
      }

      private function set eventBus(value:EventBus):void {
         _eventBusRef.object = value;
      }
   } // class

}

import com.ease.util.Diagnostics;
import com.ease.util.MixedUtil;
import com.ease.util.ReflectionUtil;
import com.ease.util.Set;
import com.ease.util.TaggedPropertyInfo;
import com.ease.framework.*;

import flash.events.IEventDispatcher;
import flash.utils.Dictionary;

import mx.collections.ArrayCollection;
import mx.collections.IList;
import mx.events.PropertyChangeEvent;
import mx.utils.StringUtil;


/**
 * Encapsulates and carries out a property injection job.
 */
class PropertyInjectionJob
{
   private var _numObjectsInjected:int = 0;
   private var _isPropertyCollectionType:Boolean;
   private var _typeTransformIsUnnecessary:Boolean = false;
   private var _appendOnly:Boolean;
   private var _numDirectoriesReturned:int = 0;
   private var _hasModifiedTargetProperty:Boolean = false;
   private var _fallbackDirectory:IObjectDirectory;

   public static var TYPE_TRANSFORMER:TypeTransformManager =
      TypeTransformManager.instance;
   public static var originalByTransformed:Dictionary = new Dictionary( true );

   /**
    * Since we're holding a strong reference here, instances of this class should only
    * exist temporarily (i.e., for the duration of the job), or else the injection
    * targets will never be released.
    */
   public var target:Object;
   public var propertyInfo:TaggedPropertyInfo;
   public var spec:PropertyInjectionObjectSpec;
   public var directories:Array/*IObjectDirectory*/;
   public var ticket:InjectionTicket;

   /**
    * Initializes an instance of this class.
    *
    * @param appendOnly
    *    If false, the target property is reset before injecting any objects. In that
    *    case, if no spec-matching objects are found, the target property is reset to
    *    empty set or null.
    */
   public function PropertyInjectionJob( target:Object, propertyInfo:TaggedPropertyInfo,
                                         spec:PropertyInjectionObjectSpec,
                                         directories:Array/*IObjectDirectory*/,
                                         appendOnly:Boolean = false,
                                         fallbackDirectory:IObjectDirectory = null ) {
      this.target = target;
      this.propertyInfo = propertyInfo;
      this.spec = spec;
      this.directories = directories;
      _fallbackDirectory = fallbackDirectory;
      _appendOnly = appendOnly;
      _isPropertyCollectionType =
         InjectionManager.isInjectableCollectionType( propertyInfo.propertyType );
      if (ReflectionUtil.isTypeCompatibleWith( spec.specTargetedValueType,
         typeExpectedByInjectionTarget )) {
         _typeTransformIsUnnecessary = true;
      } else if (ReflectionUtil.isValueType( typeExpectedByInjectionTarget )) {
         var msg:String = StringUtil.substitute(
            "Transform expected from {0} to {1}: transforming injection objects to " +
            "\"value\" types is not supported.",
            spec.specTargetedValueType, typeExpectedByInjectionTarget );
         Diagnostics.assert( false, msg );
      }
   }

   public final function start():void {
      if (this.ticket.canceled) {
         // the job was canceled even before it was started
         return;
      }
      var currentSet:Array/*?*/ = this.currentObjectSet;
      if (! _isPropertyCollectionType && _appendOnly && currentSet.length != 0) {
         // It's a non-collection (i.e., single value) property, is already set (with a
         // non-null value) and we're not allowed to reset it, so what can you do about it?
         // Just return.
         return;
      }

      for each (var directory:IObjectDirectory in this.directories) {
         var moduleReference:Object = this.target;
         directory.getObjects( this.spec, onObjectsRetrieval, moduleReference,
            currentSet );
      }
   }

   private function get currentObjectSet():Array/*?*/ {
      var currentPropVal:Object = target[propertyInfo.name];
      var currentSet:Array/*?*/ = MixedUtil.objectAsArray( currentPropVal );
      return currentSet;
   }

   private function get typeExpectedByInjectionTarget():Class {
      if (spec.targetExpectedArrayElementType) {
         return spec.targetExpectedArrayElementType;
      }
      return propertyInfo.propertyType;
   }

   private function getObjects( directory:IObjectDirectory ):void {
      var moduleReference:Object = target;
      directory.getObjects( spec, onObjectsRetrieval, moduleReference,
         this.currentObjectSet );
   }

   private function onObjectsRetrieval( objsToInject:Array/*?*/,
                                        error:Error = null ):void {
      _numDirectoriesReturned++;
      if (this.ticket.canceled) {
         cleanup();
         return;
      }
      if (error != null) {
         onError( error );
      }
      if (objsToInject.length != 0) {
         transformObjectsIfNecessary( objsToInject );
         injectObjectSet( objsToInject );
      }
      var directoryCount:int = this.directories.length;
      if (_numDirectoriesReturned == directoryCount &&
         _numObjectsInjected == 0 &&
         _fallbackDirectory != null) {
         // If no other directory responded positively to our request, we resort to our
         // fallback option.
         getObjects( _fallbackDirectory );
         return;
      }
      if (_numDirectoriesReturned < directoryCount) {
         return;
      }
      // All directories have come back with their answers.

      if (_numObjectsInjected == 0) {
         // No directory gave us any objects, so we may need to reset the target property
         // to an empty set (if reset is specified/enabled).
         injectObjectSet( [] );
      }
      if (! _hasModifiedTargetProperty) {
         invokeCallbackOnTarget( this.spec.postInjectionSkipCallbackName );
      }
      invokeCallbackOnTarget( this.spec.postInjectionFinalizeCallbackName );
      cleanup();
   }

   private function cleanup():void {
      this.target = null;
   }

   private function invokeCallbackOnTarget( methodName:String ):void {
      if (methodName == null) {
         return;
      }
      var callback:Function = this.target[methodName];
      if (callback == null) {
         return;
      }
      callback();
   }

   private function onError( error:Error ):void {
      var methodName:String =
         ReflectionUtil.getSingleTaggedMethod( target, Constants.TAG_ERROR_HANDLER );
      if (methodName == null) {
         return;
      }
      target[methodName]( error );
   }

   private function transformObjectsIfNecessary( objsToInject:Array ):Array {
      if (objsToInject.length == 0) {
         return [];
      }
      var object:*;
      var transform:*;
      if (spec.sourceProperty) {
         for (var i:int = 0; i < objsToInject.length; i++) {
            object = objsToInject[i];
            transform = object[spec.sourceProperty];
            if (object != transform) {
               originalByTransformed[transform] = object;
               objsToInject[i] = transform;
            }
         }
      }
      if (! _typeTransformIsUnnecessary) {
         for (i = 0; i < objsToInject.length; i++) {
            object = objsToInject[i];
            transform =
               TYPE_TRANSFORMER.transform( object, typeExpectedByInjectionTarget );
            if (object != transform) {
               originalByTransformed[transform] = object;
               objsToInject[i] = transform;
            }
         }
      }
      return objsToInject;
   }

   /**
    * The given array may be the result of just one of the many callbacks (corresponding
    * to the different object directories).
    *
    * <p>Any target-property setting must go through this method.
    */
   private function injectObjectSet( objsToInject:Array ):void {
      // Do not trivially return even if the given object set is empty; it might be a
      // request to reset the target property.
      try {
         this.ticket.isPropertySettingInProgress = true;
         if (_isPropertyCollectionType) {
            var countInjected:int = injectCollectionTypeProperty( objsToInject );
            _numObjectsInjected += countInjected;
            return;
         }
         var value:Object = (objsToInject.length == 0)? null : objsToInject[0];
         injectScalarTypeProperty( value );
      } catch (error:Error) {
         throw error;
      } finally {
         this.ticket.isPropertySettingInProgress = false;
      }
   }

   private function injectScalarTypeProperty( objToInject:Object ):void {
      if (_numObjectsInjected > 0) {
         return;
      }
      if (objToInject == null && _appendOnly) {
         return;
      }
      var propName:String = propertyInfo.name;
      var target:Object = this.target;
      if (ReflectionUtil.getPropertyAccessModifier( propName, target ) !=
         ReflectionUtil.ACCESS_WRITEONLY) {
         var currentPropVal:Object =
            ReflectionUtil.getPropertyValue( target, propName );
         if (MixedUtil.areIuidEquivalent( currentPropVal, objToInject )) {
            if (objToInject != null) {
               // we can effectively assume that the object has been injected
               _numObjectsInjected++;
            }
            return;
         }
      }
      target[propName] = objToInject;
      _hasModifiedTargetProperty = true;
      if (objToInject != null) {
         _numObjectsInjected++;
      }
   }

   private function injectCollectionTypeProperty( objsToInject:Array/*?*/ ):int {
      var propType:Class = this.propertyInfo.propertyType;
      var propName:String = this.propertyInfo.name;
      if (ReflectionUtil.getPropertyAccessModifier( propName, target ) !=
         ReflectionUtil.ACCESS_WRITEONLY) {
         var currentPropVal:Object =
            ReflectionUtil.getPropertyValue( target, propName );
         if (MixedUtil.areEquivalentCollections( objsToInject, currentPropVal,
            MixedUtil.IUID_EQUIVALENCE )) {
            // The current property value has the same items in the same order as the
            // newly retrieved object set, so no need to disturb the target. The objects
            // can effectively be assumed to have been injected.
            return objsToInject.length;
         }
      }
      // We reassign the value (only) if we are allowed to reset its current value and
      // if we haven't injected any items before (for the current job).
      var shouldReassign:Boolean = !_appendOnly && _numObjectsInjected == 0;
      if (! shouldReassign && objsToInject.length == 0) {
         // There's nothing to inject and we can't reset the property, so we got nothing
         // to do.
         return 0;
      }
      var count:int;
      if (propType == Array) {
         count = injectArrayTypeProperty( propName, objsToInject, shouldReassign );
      } else {
         count = injectNonArrayCollectionTypeProperty( propName, propType, objsToInject,
            shouldReassign );
      }
      return count;
   }

   private function injectArrayTypeProperty( propName:String, objsToInject:Array,
                                             shouldReassign:Boolean ):int {
      var target:Object = this.target;
      var count:int = objsToInject.length;
      // The current value of the property should be looked up only if necessary
      // (since there may not be any getters of the property); hence target[propName]
      // is used in the 2nd test-expression here.
      if (shouldReassign || target[propName] == null) {
         target[propName] = objsToInject;
         _hasModifiedTargetProperty = true;
         return count;
      }
      var arrayVal:Array = target[propName] as Array;
      arrayVal = arrayVal.concat( objsToInject );
      // The property owner (i.e., the target object) won't know that the value has
      // changed, since the property is an Array, not an ArrayCollection, so we
      // reassign the value with the newly constructed array.
      target[propName] = arrayVal;
      _hasModifiedTargetProperty = true;
      return count;
   }

   private function injectNonArrayCollectionTypeProperty( propName:String, propType:Class,
                                                          objsToInject:Array,
                                                          shouldReassign:Boolean ):int {
      var count:int = objsToInject.length;
      if (propType == ArrayCollection) {
         if (shouldReassign || target[propName] == null) {
            target[propName] = new ArrayCollection( objsToInject );
            _hasModifiedTargetProperty = true;
            return count;
         }
      }
      var list:IList;
      if (shouldReassign) {
         list = new propType() as IList;
      } else { // in-place modification (more accurately, append) case
         list = target[propName];
      }
      if (list == null) {
         Diagnostics.assert(
            false, "Can't handle types other than Array & IList; found: " + propType );
         return 0;
      }
      for each (var arrayElem:Object in objsToInject) {
         list.addItem( arrayElem );
      }
      if (objsToInject.length > 0) {
         _hasModifiedTargetProperty = true;
      }
      if (shouldReassign) {
         target[propName] = list;
         _hasModifiedTargetProperty = true;
      }
      return count;
   }
}

/**
 * Listens to property changes on an injection target object and initiates re-injections.
 *
 * <p>No strong references to the target object should be maintained in this class.
 */
class ObjectMonitor
{
   /**
    * Must be of the form:
    *    <p><code>function foo( object:Object, propsToExclude:Set ) { ... }</code>
    *
    * <p>where the 2nd arg is the set of names of the properties that should be excluded
    * from injection.
    */
   public var propertyInjector:Function;
   public var propsToMonitor:Set;

   public final function start( object:IEventDispatcher ):void {
      if (object == null) {
         return;
      }
      // no strong reference should be created to the object in this class
      object.addEventListener( PropertyChangeEvent.PROPERTY_CHANGE,
         onPropChange, false, 0, true );
   }

   public final function stop( object:IEventDispatcher ):void {
      if (object == null) {
         return;
      }
      object.removeEventListener( PropertyChangeEvent.PROPERTY_CHANGE, onPropChange );
   }

   private function onPropChange( ev:PropertyChangeEvent ):void {
      var object:Object = ev.source;
      var propName:String = String( ev.property );
      if (! propsToMonitor.contains( propName )) {
         return;
      }
      var propsToExclude:Set/*String*/ = new Set();
      // We don't want to cause re-assessment of the very property that is the trigger.
      // That would be bad for performance reasons, and we also don't want to overwrite
      // the properties that are explicitly set by the client (until a change in some
      // other property triggers their reinjection, at which point its okay to bring
      // these properties into compliance with the dependent properties).
      propsToExclude.add( propName );
      propertyInjector( object, propsToExclude );
   }
}

class InjectablePropertyInfo
{
   private var _dynamicInjectionTicket:InjectionTicket;

   public var propertyInfo:TaggedPropertyInfo;
   public var spec:PropertyInjectionObjectSpec;

   public function InjectablePropertyInfo( propertyInfo:TaggedPropertyInfo,
                                           spec:PropertyInjectionObjectSpec ) {
      this.propertyInfo = propertyInfo;
      this.spec = spec;
   }

   public function get dynamicInjectionTicket():InjectionTicket {
      if (_dynamicInjectionTicket == null) {
         _dynamicInjectionTicket = new InjectionTicket();
      }
      return _dynamicInjectionTicket;
   }

   public final function resetDynamicInjectionTicket():void {
      if (_dynamicInjectionTicket) {
         _dynamicInjectionTicket.canceled = true;
      }
      _dynamicInjectionTicket = new InjectionTicket();
   }
}

/**
 * Don't keep a reference to the target object in this class; that would keep the object
 * from going away.
 */
class InjectionTargetInfo
{
   public var staticInjectionTicket:InjectionTicket = new InjectionTicket();
   [ArrayElementType("com.ease.util.TaggedPropertyInfo")]
   public var staticallyInjectedProps:Array = [];

   /**
    * A prop-name to InjectablePropertyInfo map.
    */
   public var propsToReinjectOnChanges:Dictionary/*(String, InjectablePropertyInfo)*/;

   public final function resetStaticInjectionTicket():void {
      staticInjectionTicket.canceled = true;
      staticInjectionTicket = new InjectionTicket();
   }

   public final function resetInjectionTickets():void {
      resetStaticInjectionTicket();
      for each (var ipi:InjectablePropertyInfo in propsToReinjectOnChanges) {
         ipi.resetDynamicInjectionTicket();
      }
   }
}

class InjectionTicket
{
   public var canceled:Boolean = false;

   /**
    * Indicates whether the target property is currently being set (i.e., the setter is
    * executing).
    */
   public var isPropertySettingInProgress:Boolean = false;
}
